const chalk = require('chalk') // 命令行颜色
const ora = require('ora') // 加载流程动画
const spinner_style = require('./spinner_style') // 加载动画样式
const shell = require('shelljs') // 执行shell命令
const { NodeSSH } = require('node-ssh') // ssh连接服务器
const inquirer = require('inquirer') // 命令行交互
const zipFile = require('compressing') // 压缩zip
const path = require('path')
const dayjs = require('dayjs')
const notice = require('./notice')
const exec = require('child_process').exec
const Validate = require('async-validator').default
const descriptor = require('./descriptor')
const { models, builtIn } = require('./config')
const { to, toType, defaultLog, errorLog, successLog, timeLog, timeEndLog, geoBuiltIn } = require('./utils.js')

// 参数验证
const configSchema = new Validate(descriptor)

class Deploy {
  constructor(options = {}) {
    this.options = options
    this.SSH = new NodeSSH()
    this.deployConfig
    this.name

    // 工作目录
    this.runPath = process.cwd()

    // 参数校验
    this._validate()
      .then(() => {
        try {
          // 开始运行
          this._run()
        } catch (error) {
          process.exitCode = 1 // 退出流程
        }
      })
      .catch(() => {
        process.exitCode = 1 // 退出流程
      })
  }

  static instantiate() {
    return new Deploy(...arguments)
  }

  /**
   * 运行
   * @memberof Deploy
   */
  async _run() {
    const selectList = {
      type: 'list',
      message: '请选择发布环境',
      name: 'env',
      choices: this.options.choices,
    }

    // 执行交互后 启动发布程序
    inquirer.prompt(selectList).then(async (answers) => {
      const match = this.options.choices.find((e) => e.value === answers.env)

      // 格式化，把对象转数组
      this.options.deployList = this.options.deployList.reduce(
        (prev, cur) => ({ ...prev, [cur.VALUE]: cur }),
        {}
      )
      this.deployConfig = this.options.deployList[answers.env]
      this.model = this.deployConfig.RUN_MODEL
      this._checkConfig(this.deployConfig) // 检查
      this.name = `${this.options.name}-${match.name}`

      if (this.deployConfig.ASSETS_PATH) {
        this.distDir = path.resolve(this.runPath, this.deployConfig.ASSETS_PATH) // 待打包
        this.distZipPath = path.resolve(this.runPath, `dist.zip`) // 打包后地址(dist.zip是文件名,不需要更改, 主要在config中配置 PATH 即可)
      }

      // 部署
      const [err, res] = await to(this._autoDeploy())
      if (!err && res) {
        notice(
          `${this.name}${models[this.model].msg}`,
          `${this.name}${models[this.model].msg}完成，${
            this.model <= 3 ? '点击可前往该目录' : ''
          }`,
          (options) => {
            if (options.action === 'clicked' && this.model <= 3) {
              exec(
                `explorer.exe /select,"${path.resolve(
                  this.runPath,
                  'dist.zip'
                )}"`
              )
            }
          }
        )
      }

      // 断开连接
      this.SSH.dispose()

      process.exitCode = 0 // 正常退出
    })
  }

  /**
   * 部署
   * @returns
   * @memberof Deploy
   */
  async _autoDeploy() {
    const { model } = this
    let runProcess = []

    if (model !== 4) {
      for (let i = 0; i < model; i++) {
        const { run, label } = builtIn[i + 1]
        runProcess.push(geoBuiltIn(this, run, label))
      }
    } else {
      if (!toType(this.deployConfig.SERVER_CMD, 'Array')) {
        this.deployConfig.SERVER_CMD = []
      }
      runProcess = this.deployConfig.SERVER_CMD.map(e => {
        return geoBuiltIn(this, e)
      })
    }

    console.log(chalk.yellow(`--------->  前端自动部署工具  <---------`))
    timeLog('总时间：')
    for (const e of runProcess) {
      timeLog(e.label)
      await e.fn.call(this)
      timeEndLog(e.label)
      console.log(`\n`)
    }

    successLog('--------->  部署成功!  <---------')
    timeEndLog('总时间：')
    successLog(dayjs().format('YYYY-MM-DD HH:mm:ss'))

    return true
  }

  // 验证参数
  async _validate() {
    let loading
    const [errObj] = await to(configSchema.validate(this.options), {
      before: () => {
        loading = ora(defaultLog('正在校验参数中')).start()
        loading.spinner = spinner_style.betaWave
      },
      after: () => {
        loading.stop()
      },
    })
    if (!errObj) {
      successLog('参数校验成功')
      return true
    } else {
      notice(`参数校验`, '参数校验错误，请查看控制台输出日志内容进行修正！')
      const err = (errObj.errors || []).reduce((prev, cur) => {
        return prev + `字段：${cur.field}，失败原因：${cur.message}\n\r`
      }, '\n\r')
      errorLog(err)
      return Promise.reject()
    }
  }

  /**
   * 打包阶段
   * @returns
   * @memberof Deploy
   */
  async _compileDist() {
    let loading
    shell.cd(path.resolve(this.runPath))

    // 执行shell 打包命令
    const [err, res] = await to(shell.exec(this.deployConfig.SCRIPT), {
      before: () => {
        loading = ora(defaultLog('项目开始打包')).start()
        loading.spinner = spinner_style.betaWave
      },
      after: () => {
        loading.stop()
      },
    })
    if (!err && res.code === 0) {
      successLog('项目打包成功!')
    } else {
      notice(`${this.name}的项目打包错误`, '项目打包失败, 请重试!')
      errorLog('项目打包失败, 请重试!')
      return Promise.reject()
    }
  }

  /**
   * 代码压缩阶段
   * @returns
   * @memberof Deploy
   */
  async _zipDist() {
    let loading
    // 文件夹目录
    const [err] = await to(
      zipFile.zip.compressDir(this.distDir, this.distZipPath), {
        before: () => {
          loading = ora(defaultLog('项目开始压缩')).start()
          loading.spinner = spinner_style.betaWave
        },
        after: () => {
          loading.stop()
        },
      }
    )
    if (!err) {
      successLog('压缩成功!')
    } else {
      errorLog(err)
      notice(`${this.name}的压缩代码错误`, '压缩失败, 退出程序!')
      errorLog('压缩失败, 退出程序!')
      return Promise.reject()
    }
  }

  /**
   * 连接服务器阶段
   * @returns
   * @memberof Deploy
   */
  async _connectSSH() {
    if (this.SSH.isConnected()) {
      return successLog('服务器已经连接，无序再次连接')
    }
    let loading
    const [err] = await to(
      this.SSH.connect({
        host: this.deployConfig.SERVER_PATH,
        username: this.deployConfig.SSH_USER,
        password: this.deployConfig.PASSWORD, // 密码登录 方式二
      }),
      {
        before: () => {
          loading = ora(defaultLog('正在连接服务器')).start()
          loading.spinner = spinner_style.betaWave
        },
        after: () => {
          loading.stop()
        },
      }
    )
    if (!err) {
      successLog('服务器连接成功!')
      this.isConnect = true
    } else {
      errorLog(err)
      notice(`${this.name}的连接服务器错误`, 'SSH连接失败!')
      errorLog('SSH连接失败!')
      return Promise.reject()
    }
  }

  /**
   * 传送zip文件到服务器
   * @returns
   * @memberof Deploy
   */
  async _uploadZipBySSH() {
    // 连接ssh
    await this._connectSSH()
    // 线上目标文件清空
    await this._clearOldFile()

    let loading
    const [err] = await to(
      this.SSH.putFiles([
        // local 本地 ; remote 服务器 ;
        {
          local: this.distZipPath,
          remote: this.deployConfig.PATH + '/dist.zip',
        },
      ]),
      {
        before() {
          loading = ora(defaultLog('准备上传文件')).start()
          loading.spinner = spinner_style.betaWave
        },
        after() {
          loading.stop()
        },
      }
    )
    if (!err) {
      successLog('上传成功!')
      loading.text = '正在解压文件'
      await this._runCommand('unzip ./dist.zip') // 解压
      await this._runCommand(`rm -rf ${this.deployConfig.PATH}/dist.zip`) // 解压完删除线上压缩包
      await this._runCommand(
        `mv -f ${this.deployConfig.PATH}/${this.deployConfig.ASSETS_PATH}/*  ${this.deployConfig.PATH}`
      )
      await this._runCommand(`rm -rf ${this.deployConfig.PATH}/${this.deployConfig.ASSETS_PATH}`) // 移出后删除 dist 文件夹
    } else {
      errorLog(err)
      notice(`${this.name}的连接SSH错误`, '上传失败!')
      errorLog('上传失败!')
      return Promise.reject()
    }
  }

  /**
   * 线上执行命令
   * @param {String} command 命令操作 如 ls
   * @memberof Deploy
   */
  async _runCommand(command) {
    if (!toType(command, 'String')) {
      notice(`${this.name}的自定义指令`, `"${command}"指令操作失败!`)
      errorLog('指令必须是字符串')
      return Promise.reject()
    }
    let loading
    const [error, result] = await to(
      this.SSH.exec(command, [], {
        cwd: this.deployConfig.PATH,
        stream: 'both'
      }),
      {
        before: () => {
          loading = ora(defaultLog(`正在执行${command}指令`)).start()
          loading.spinner = spinner_style.betaWave
        },
        after: () => {
          loading.stop()
        },
      }
    )

    if (!error) {
      successLog(`\n ${result.stdout}，\n ${command}指令成功! \n`)
    } else {
      errorLog(error)
      notice(`${this.name}的自定义指令`, `"${command}"指令操作失败!`)
      errorLog(`自定义指令，"${command}"指令操作失败!`)
      return Promise.reject()
    }
  }

  /**
   * 清空线上目标目录里的旧文件
   * @memberof Deploy
   */
  async _clearOldFile() {
    try {
      const commands = ['ls', 'rm -rf *']
      await Promise.all(commands.map((it) => this._runCommand(it)))
    } catch (error) {
      errorLog(error)
      notice(`${this.name}的操作线上目录错误`, '操作失败!')
      errorLog('清空线上目标目录里的旧文件，操作失败!')
      return Promise.reject()
    }
  }

  /**
   * 开始前的配置检查
   * @param {Object} conf 配置对象
   * @memberof Deploy
   */
  async _checkConfig(conf) {
    const checkArr = Object.entries(conf)
    checkArr.map((it) => {
      const key = it[0]
      if (key === 'PATH' && conf[key] === '/') {
        // 上传zip前会清空目标目录内所有文件
        notice(`${this.name}的配置检查错误`, 'PATH 不能是服务器根目录!')
        errorLog('PATH 不能是服务器根目录!')
        process.exitCode = 1 // 退出流程
      }
      if (!conf[key]) {
        notice(`${this.name}的配置检查错误`, `配置项 ${key} 不能为空`)
        errorLog(`配置项 ${key} 不能为空`)
        process.exitCode = 1 // 退出流程
      }
    })
  }
}

module.exports = Deploy.instantiate
